Skip to content

perf: skip archived repos without fetching them - #1065

Open
rafaelleonardocruz wants to merge 2 commits into
github-community-projects:main-enterprisefrom
rafaelleonardocruz:skip-archived-repos
Open

perf: skip archived repos without fetching them#1065
rafaelleonardocruz wants to merge 2 commits into
github-community-projects:main-enterprisefrom
rafaelleonardocruz:skip-archived-repos

Conversation

@rafaelleonardocruz

Copy link
Copy Markdown

Problem

updateRepos already skips archived repos — #991 added:

if (isArchived && !shouldUnarchive) {
  this.log.debug(`Skipping repo/child plugin updates for archived repo ${repo.repo}`)
  return
}

But that check runs after archivePlugin.getState(), which calls repos.get. So every archived repo still costs one API call per full sync, purely to learn something the caller already knew.

eachRepositoryRepos paginates GET /installation/repositories, and that response already carries archived for every repo. Today only owner.login and name are read from it.

Why it matters

On an installation, the rate limit — not fan-out concurrency — is what bounds how long a full sync takes. Wasted calls translate directly into wall-clock time.

In the organization where I hit this, 2228 of 3013 repos (74%) are archived, so the clear majority of the sync's request budget went to fetching repos in order to skip them. A secondary effect: each archived repo that isn't skipped early still emits 403s from the repository and labels plugins, which buries genuine errors in the log — I had to reconstruct the ratio arithmetically to tell signal from noise while debugging an unrelated problem.

Change

Thread archived from the listing through checkAndProcessRepo into updateRepos, and skip before issuing any request.

if (repo.archived === true) {
  const desiredArchiveState = new Archive(this.nop, this.github, repo, repoConfig, this.log).getDesiredArchiveState()
  if (desiredArchiveState !== false) {
    this.log.debug(`Skipping archived repo ${repo.repo} without fetching it`)
    return
  }
}

Net effect: one fewer API call per archived repo per full sync.

Correctness notes

  • Unarchiving still works. The skip is conditional on the desired state, so an explicit archived: false in config is still processed — that is a request to unarchive. getDesiredArchiveState() reads config only and issues no request, so the guard stays free.
  • Other call sites are unaffected. Settings.sync and syncSelectedRepos build repo without an archived field. Those pass undefined, the new guard does not fire, and they fall through to the existing isArchived check from Bug/archived repo #991 — same behaviour as today.
  • Placement. The guard sits after the suborg and repo-override merge, so repoConfig is fully resolved before the desired state is read.

Tests

Four added to test/unit/lib/settings.test.js, asserting on whether repos.get was called:

  • skips with no fetch when the caller reports the repo archived
  • still fetches when config asks to unarchive it
  • still fetches when the caller does not report archived state
  • archived is threaded from the repository listing into updateRepos

npx jest --roots=lib --roots=test/unit141 passing, 0 failing. eslint clean on both changed files (the 69 pre-existing semi/quotes issues elsewhere in the test file are untouched).

One note on the test setup, in case it helps future tests: the file's createSettings helper passes mockSubOrg, which sets subOrgConfigMap and makes updateRepos return early for any repo outside that suborg. These tests construct Settings without a suborg for that reason.

`updateRepos` already skips archived repos (github-community-projects#991), but only after
`archivePlugin.getState()` has spent a `repos.get` on each one. That call is
avoidable: `GET /installation/repositories`, which `eachRepositoryRepos` already
paginates, reports `archived` in its payload.

Thread that flag through `checkAndProcessRepo` into `updateRepos` and skip
before issuing any request. The saving is one API call per archived repo per
full sync. In the organization where I found this, 2228 of 3013 repos (74%) are
archived, so the majority of a full sync's rate-limit budget was spent fetching
repos only to skip them — and on an installation the rate limit, not
concurrency, is what bounds how long a full sync takes.

The skip is conditional on the desired state, so an explicit `archived: false`
in config is still processed: that is a request to unarchive.
`getDesiredArchiveState()` reads config only and issues no request. Callers that
do not know the archived state (single-repo webhook syncs) pass `undefined` and
keep the existing behaviour, falling through to the `isArchived` check from github-community-projects#991.

Four tests added: skip with no fetch when the caller reports archived, still
fetch when config asks to unarchive, still fetch when the caller does not report
the state, and the flag being threaded from the listing into `updateRepos`.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
Co-Authored-By: claude-code <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 19, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes full-sync performance by using the archived flag already present in the GET /installation/repositories listing payload to skip processing archived repositories before making per-repo API calls.

Changes:

  • Thread repository.archived from eachRepositoryReposcheckAndProcessRepoupdateRepos.
  • Add an early guard in updateRepos to skip archived repos without calling repos.get, unless config explicitly requests unarchiving.
  • Add unit tests asserting when repos.get is (and is not) called and that archived is correctly threaded through.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/settings.js Pass archived from repo listing into updateRepos and add an early skip to avoid unnecessary API calls for archived repos.
test/unit/lib/settings.test.js Add unit tests covering early-skip behavior and verifying archived is threaded into updateRepos.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/settings.js Outdated
Two review findings, both valid.

Copilot: threading `archived` on the repo object leaks it into plugins. The
Repository plugin does `Object.assign({}, settings, repo)` (plugins/repository.js:48)
with `repo` last, and later `repos.update(this.settings)` (:215) — so an
`archived` key on the ref lands in the update payload. In the unarchive flow that
is destructive: the repo is unarchived, then the Repository plugin PATCHes
`archived: true` back and re-archives it. `repos.get(this.repo)` (:67) would also
receive a stray parameter. `archived` is now a separate argument to updateRepos
and never touches the ref; plugins keep receiving a bare { owner, repo }.

Second finding: the guard only covered the `repoConfig` branch. Without a
repoConfig — a labels-only configuration, for instance — the else branch ran every
child plugin with no archive check, so an archived repo still received forbidden
writes. Added the same guard there, conditional on `archived !== false` so it
costs nothing on the full-sync path: `false` from the listing needs no request,
and `true` already returned earlier unless an unarchive was requested. Only a
caller that does not know the state (single-repo webhook sync) pays one repos.get.

Also dropped the duplicate Archive instantiation — the hoisted one is reused.

Tests 141 -> 143. Two new: the no-repoConfig path skips child plugins for an
archived repo, and `archived` does not appear on the ref passed to updateRepos.
The threading test now asserts the argument position rather than a merged object.
The labels stub is deliberately complete (endpoint.merge + paginate) so the
no-write assertion fails loudly instead of passing because the plugin crashed.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
Co-Authored-By: claude-code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants